import isRegexp from "is-regexp";
import isObject from "is-obj";

const getOwnEnumPropSymbols = (object) =>
  Object.getOwnPropertySymbols(object).filter((keySymbol) =>
    Object.prototype.propertyIsEnumerable.call(object, keySymbol)
  );

export default function stringifyObject(input, options, pad) {
  const seen = [];

  return (function stringify(input, options = {}, pad = "") {
    const indent = options.indent || "\t";

    let tokens;
    if (options.inlineCharacterLimit === undefined) {
      tokens = {
        newline: "\n",
        newlineOrSpace: "\n",
        pad,
        indent: pad + indent,
      };
    } else {
      tokens = {
        newline: "@@__STRINGIFY_OBJECT_NEW_LINE__@@",
        newlineOrSpace: "@@__STRINGIFY_OBJECT_NEW_LINE_OR_SPACE__@@",
        pad: "@@__STRINGIFY_OBJECT_PAD__@@",
        indent: "@@__STRINGIFY_OBJECT_INDENT__@@",
      };
    }

    const expandWhiteSpace = (string) => {
      if (options.inlineCharacterLimit === undefined) {
        return string;
      }

      const oneLined = string
        .replace(new RegExp(tokens.newline, "g"), "")
        .replace(new RegExp(tokens.newlineOrSpace, "g"), " ")
        .replace(new RegExp(tokens.pad + "|" + tokens.indent, "g"), "");

      if (oneLined.length <= options.inlineCharacterLimit) {
        return oneLined;
      }

      return string
        .replace(
          new RegExp(tokens.newline + "|" + tokens.newlineOrSpace, "g"),
          "\n"
        )
        .replace(new RegExp(tokens.pad, "g"), pad)
        .replace(new RegExp(tokens.indent, "g"), pad + indent);
    };

    if (seen.includes(input)) {
      return '"[Circular]"';
    }

    if (
      input === null ||
      input === undefined ||
      typeof input === "number" ||
      typeof input === "boolean" ||
      typeof input === "function" ||
      typeof input === "symbol" ||
      isRegexp(input)
    ) {
      return String(input);
    }

    if (input instanceof Date) {
      return `new Date('${input.toISOString()}')`;
    }

    if (Array.isArray(input)) {
      if (input.length === 0) {
        return "[]";
      }

      seen.push(input);

      const returnValue =
        "[" +
        tokens.newline +
        input
          .map((element, i) => {
            const eol =
              input.length - 1 === i
                ? tokens.newline
                : "," + tokens.newlineOrSpace;

            let value = stringify(element, options, pad + indent);
            if (options.transform) {
              value = options.transform(input, i, value);
            }

            return tokens.indent + value + eol;
          })
          .join("") +
        tokens.pad +
        "]";

      seen.pop();

      return expandWhiteSpace(returnValue);
    }

    if (isObject(input)) {
      let objectKeys = [...Object.keys(input), ...getOwnEnumPropSymbols(input)];

      if (options.filter) {
        objectKeys = objectKeys.filter((element) =>
          options.filter(input, element)
        );
      }

      if (objectKeys.length === 0) {
        return "{}";
      }

      seen.push(input);

      const returnValue =
        "{" +
        tokens.newline +
        objectKeys
          .map((element, i) => {
            const eol =
              objectKeys.length - 1 === i
                ? tokens.newline
                : "," + tokens.newlineOrSpace;
            const isSymbol = typeof element === "symbol";
            const isClassic = !isSymbol && /^[a-z$_][$\w]*$/i.test(element);
            const key =
              isSymbol || isClassic ? element : stringify(element, options);

            let value = stringify(input[element], options, pad + indent);
            if (options.transform) {
              value = options.transform(input, element, value);
            }

            return tokens.indent + String(key) + ": " + value + eol;
          })
          .join("") +
        tokens.pad +
        "}";

      seen.pop();

      return expandWhiteSpace(returnValue);
    }

    input = String(input).replace(/[\r\n]/g, (x) =>
      x === "\n" ? "\\n" : "\\r"
    );

    if (options.singleQuotes === false) {
      input = input.replace(/"/g, '\\"');
      return `"${input}"`;
    }

    input = input.replace(/\\?'/g, "\\'");
    return `'${input}'`;
  })(input, options, pad);
}
